<header>
<h1>正規表示式簡介</h1>
</header>

<p><b>正規表示式</b>（regular expression，常簡寫作RegExp或regex，尚有「規則運算式」、「正則表達式」等多種譯法）是一種定義文字比對規則的語言，可用於尋找、取代文字。正規表示式應用極為廣泛，大多數程式語言都支援用正規表示式做文字操作，許多應用程式也可以使用，且幾乎不可能被淘汰，是<a href="https://ckhung0.blogspot.com/2016/04/learn-to-code.html">非常值得投資學習的工具</a>。</p>
<p>正規表示式的參考資源極多，以下提供一些通用基本觀念，以便沒有寫程式經驗的朋友瞭解一些簡單但威力無窮的應用方式，及實際用於資料檢索。文末也提供一些參考資源，供有興趣的朋友深入鑽研。</p>

<h2>語法簡介</h2>
<p>正規表示式中有一些特定符號或符號序列具有特殊意義，一些常用符號簡介如下：</p>

<ul>
<li>「<code>.</code>」：表示一個任意字元，例如「<code>a.c</code>」可匹配「aac」、「abc」。
  <ul>
  <li>預設情況下「<code>.</code>」代表的任意字元不包括換行字元，但也可設定為包括。</li>
  </ul>
</li>
<li>「<code>{m,n}</code>」：表示前一字元重複m至n次。例如「<code>go{2,5}gle</code>」可匹配「google」、「gooogle」、「goooogle」、「gooooogle」。
  <ul>
  <li>逗號和n都可以省略，但m不能省，例如「<code>go{3}gle</code>」表示「o」重複3次，「<code>go{5,}gle</code>」表示「o」重複5次以上，但「<code>go{0,3}gle</code>」不能寫成「<code>go{,3}gle</code>」。</li>
  </ul>
</li>
<li>「<code>*</code>」：表示前一字元重複0次以上，相當於「<code>{0,}</code>」。例如「<code>噗哈*</code>」可匹配「噗」、「噗哈」、「噗哈哈」、……。</li>
<li>「<code>+</code>」：表示前一字元重複1次以上，相當於「<code>{1,}</code>」。例如「<code>哈+囉</code>」可匹配「哈囉」、「哈哈囉」、「哈哈哈囉」、……。</li>
<li>「<code>?</code>」：表示前一字元重複0至1次，相當於「<code>{0,1}</code>」。例如「<code>colou?r</code>」可匹配「color」或「colour」。
  <ul>
  <li>若「<code>?</code>」接在重複次數後面，則其意義改為表示「優先選擇最短匹配」。正規表示式引擎的預設做法是「優先選擇最長匹配」，例如對「accccc」用「<code>ac*</code>」或「<code>ac+</code>」匹配會得到「accccc」，用「<code>ac{2,4}?</code>」匹配會得到「acccc」，用「<code>ac?</code>」匹配會得到「ac」。但用「<code>ac*?</code>」或「<code>ac??</code>」匹配會得到「a」，用「<code>ac+?</code>」匹配會得到「ac」，用「<code>ac{2,4}?</code>」匹配會得到「acc」。</li>
  <li>注意：若用「<code>a*?c</code>」匹配「aaaaac」會得到「aaaaac」而不是「c」。這是因為正規表示式引擎的運作方式是「由前而後，匹配優先」，也就是由第一個字元前的位置開始，在該位置的文字若能和正規表示式匹配就取作結果並結束，不能匹配則前進至下一字元的位置嘗試。此例由最前面的a之前開始，在這個位置可匹配到「aaaaac」，因此立刻結束並得到此結果。同理，用「<code>a{1,4}?c</code>」匹配「aaaaac」會得到「aaaac」而不是「ac」，因為首次在第一個a之前無法匹配，於是前進一字，在第二個a之前的位置可匹配到「aaaac」，因此立刻結束並得到此結果。</li>
  </ul>
</li>
<li>「<code>(x)</code>」：將x視為子群組，同時記住匹配到的值供之後使用。例如「<code>(0+)7\1</code>」可匹配「070」、「00700」、「0007000」、……等等（其中「<code>\1</code>」引用第1個子群組的值）。
  <ul>
  <li>注意：子群組的值是x匹配到的值，而不是x表示式。「<code>(0+)7\1</code>」與「<code>0+70+</code>」不等價，比如後者可匹配「0070」而前者不能。</li>
  <li>許多工具在取代文字時可引用子群組的值。例如對文字「xabycccdz」用「<code>x(a*b)y(c*d)z</code>」取代為「<code>\1,\2</code>」會得到「ab,cccd」；若取代為「<code>\2,\1</code>」則會得到「cccd,ab」。有些程式（如JavaScript）不是用「<code>\1</code>」而是用「<code>$1</code>」表示。有些程式還支援其他參數，例如「<code>$&</code>」表示整個匹配字串。</li>
  <li>括號可巢狀嵌套。例如對文字「xabbcy」用「<code>x(a(b+)c)y</code>」取代為「<code>\1,\2</code>」會得到「abbc,bb」。</li>
  <li>子群組可能得到空值，也可能不存在。例如用「<code>go(o*)gle</code>」匹配「gogle」後，第1子群組是空值；用「<code>go(o)*gle</code>」匹配「gogle」後，第1子群組為不存在。有些程式能區分存在與不存在的子群組，進而設定不同的處理方式，例如Notepad++可以尋找文字「<code>go(o)*gle</code>」取代為「<code>(?1google:fake)</code>」，若輸入文字是「<code>This is gooogle.</code>」會得到「This is google.」，若是「<code>gogle</code>」則會得到「This is fake.」。</li>
  </ul>
</li>
<li>「<code>(?&lt;name&gt;x)</code>」：將x視為子群組，同時把匹配到的值記錄在名為「name」的子群組，之後可用「<code>\k&lt;name&gt;</code>」引用。例如「<code>(?&lt;zeros&gt;0+)7\k&lt;zeros&gt;</code>」可匹配「070」、「00700」、「0007000」、……等等。</li>
<li>「<code>(?:x)</code>」：將x視為子群組，但不記住x的內容。例如「<code>(?:ya)+</code>」可匹配「ya」、「yaya」、「yayaya」、……。</li>
<li>「<code>(?=x)</code>」：檢查此位置後面為x，x本身不納入匹配。例如用「<code>予(?=.*?桂枝)(?=.*?麻黃)</code>」匹配「予桂枝、麻黃」或「予麻黃、桂枝」都會得到「予」，而匹配「予桂枝、芍藥」則無結果。</li>
<li>「<code>(?!x)</code>」：檢查此位置後面非x，x本身不納入匹配。例如用「<code>予(?!.*?桂枝)(?!.*?麻黃)</code>」匹配「予桂枝、大棗」或「予麻黃、杏仁」都無結果，而匹配「予芍藥、生薑」會得到「予」。</li>
<li>「<code>(?&lt;=x)</code>」：檢查此位置前面為x，x本身不納入匹配。例如用「<code>(?&lt;=曰)夫</code>」匹配「帝曰夫道者年皆百數能有子乎」會得到「夫」，而匹配「丈夫八歲腎氣實」則無結果。</li>
<li>「<code>(?&lt;!x)</code>」：檢查此位置前面非x，x本身不納入匹配。例如用「<code>(?&lt;!白)附子</code>」匹配「用炮附子一錢」會得到「附子」，而匹配「用白附子一錢」則無結果。
  <ul>
  <li>有些正規表示式引擎不支援此類逆向檢查語法，例如較舊版本瀏覽器的JavaScript；許多正規表示式引擎雖支援此語法，但x須為固定字元數，例如可使用「<code>(?&lt;=[abc])</code>」或「<code>(?&lt;!小明|美華)</code>」，但不可使用「<code>(?&lt;=a*b)</code>」或「<code>(?&lt;!土匪|狗)</code>」。</li>
  </ul>
</li>
<li>「<code>x|y</code>」：匹配x或y。例如「<code>我(愛|喜歡)你</code>」可匹配「我愛你」或「我喜歡你」。
  <ul>
  <li>若用「<code>小美|小美愛小明</code>」匹配「我發現小美愛小明」，結果是什麼？答案是視正規表示式引擎而定，多數正規表示式引擎是「循序型」，會取第一個匹配成功的選項。就此例言，當引擎檢查到「小美」前，發現「小美」可以匹配成功，就取「小美」作為結果並略過其他選項；同樣文字如用「<code>小美愛小明|小美</code>」匹配，結果就會變成「小美愛小明」。但有少數正規表示式引擎是「貪婪型」，這種引擎會從匹配成功的選項中選擇最長的，因此結果會是「小美愛小明」。</li>
  </ul>
</li>
<li>「<code>\</code>」：對後一個字元做脫義處理，使其不被解釋為具有特殊意義。例如「<code>\.</code>」表示「.」，「<code>\\</code>」則表示「\」本身。
  <ul>
  <li>若後一個字元不具特殊意義，一般是表示相同字元，例如「<code>\,</code>」表示「,」，「<code>\人</code>」表示「人」；但「<code>\</code>」接數字或字母可能另有特殊意義，詳見後述。</li>
  </ul>
</li>
<li>「<code>\d</code>」：匹配一個數字字元，相當於「<code>[0-9]</code>」。</li>
<li>「<code>\D</code>」：匹配一個非\d的字元。（包括換行字元）</li>
<li>「<code>\s</code>」：匹配一個空白或換行字元。</li>
<li>「<code>\S</code>」：匹配一個非\s的字元。</li>
<li>「<code>\n</code>」：匹配一個行尾字元（linefeed，U+000A）。</li>
<li>「<code>\r</code>」：匹配一個回車字元（carriage return，U+000D）。
  <ul>
  <li>Windows預設換行是「\r\n」，Linux預設換行是「\n」，Mac預設換行是「\r」。</li>
  </ul>
</li>
<li>「<code>\t</code>」：匹配一個表格字元（tab，U+0009）。</li>
<li>「<code>\xFF</code>」（FF為2位數的十六進位數字）：匹配相應編碼的單個字元（<a href="https://zh.wikipedia.org/wiki/ASCII">ASCII碼</a>或環境使用的編碼）。例如「<code>\x00</code>」匹配空字元，「<code>\x20</code>」匹配半形空白字元。</li>
<li>「<code>\uFFFF</code>」（FFFF為4位數的十六進位數字）：匹配相應Unicode編碼的單個字元。例如「<code>\u4E00</code>」匹配「一」（U+4E00）。
  <ul>
  <li>此語法在不同平台有不同變形。上述是JavaScript的寫法，設定Unicode標幟後也可以使用「<code>\u{FFFF}</code>」或「<code>\u{FFFFF}</code>」；其餘如Perl、PCRE、Boost使用「<code>\x{F+}</code>」（任意位數的十六進位數字前後加花括號）；Python使用「<code>\uFFFF</code>」和「<code>\UFFFFFFFF</code>」。</li>
  </ul>
</li>
<li>「<code>\p{...}</code>」：匹配一個指定Unicode字元集合內的字元。可用的指定方式可查閱<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes">文件</a>。一些實用例子如下：
  <ul>
  <li>「<code>\p{Letter}</code>」或「<code>\p{L}</code>」：匹配任何語言的文字字元，例如漢字「一」、英文字母「a」、日文假名「あ」等等。</li>
  <li>「<code>\p{Number}</code>」或「<code>\p{N}</code>」：匹配數字字元，例如阿拉伯數字0-9、漢字零「〇」等等。</li>
  <li>「<code>\p{Punctuation}</code>」或「<code>\p{P}</code>」：匹配標點符號字元，例如「,」、「.」、「，」、「。」等等。</li>
  </ul>
</li>
<li>「<code>\P{...}</code>」：匹配一個非\p{...}的字元。</li>
<li>「<code>^</code>」：檢查此位置為字串開頭（或可設定為行首）。例如「<code>^小明</code>」匹配字串開頭（或行首）的「小明」。</li>
<li>「<code>$</code>」：檢查此位置為字串結尾（或可設定為行尾）。例如「<code>台灣。$</code>」匹配字串結尾（或行尾）的「台灣。」。</li>
<li>「<code>[...]</code>」：匹配一個字元集合中的字元，例如「<code>x[abc]y</code>」可匹配「xay」、「xby」、「xcy」。
  <ul>
  <li>「<code>[x-y]</code>」可指定字元區間。例如「<code>[a-z]</code>」可匹配「a」、「b」、「c」、…、「z」。此語法可與單個字元混用，例如「<code>[0-9A-Za-z_,]</code>」。</li>
  <li>「<code>[^...]</code>」可反轉字元集合，例如「<code>[^abc]</code>」表示匹配一個「a」、「b」、「c」以外的字元。</li>
  <li>在「<code>[...]</code>」之中除「<code>\</code>」、「<code>]</code>」、開頭的「<code>^</code>」、及可與前後字元形成區間的「<code>-</code>」以外，任何字元皆無視特殊意義，例如「<code>[.+^-]</code>」表示匹配「.」或「+」或「^」或「-」。</li>
  </ul>
</li>
</ul>

<h2>典籍檢索器無法支援的正規表示式</h2>

<p>正規表示式極為彈性，由於技術限制，笈成典籍檢索器有些功能無法完全支援所有表示式。以下是一些已知的例子：</p>

<ul>
  <li>字元集合（<code>[...]</code>）不支援比對異體字。例如「<code>乾[薑]</code>」表示搜尋「乾薑」，其中「乾」比對異體字，而「薑」不比對。這是因為像「<code>(參|耆)</code>」可用程式替換為「<code>([參参薓蔘]|[耆芪])</code>」，但「<code>[參耆]</code>」、「<code>[^參耆]</code>」則無法如此處理。如有比對異體字的需求，可用前者取代後者。</li>
  <li>重複次數運算子（<code>{m,n}</code>、<code>*</code>、<code>+</code>、<code>?</code>）不支援略過標點。例如「<code>陰*</code>」無法匹配「陽氣根於陰，陰氣根於陽」。</li>
</ul>

<h2>應用實例</h2>
<p>以下列舉一些可在本站<a href="search.html">典籍檢索器</a>使用的正規表示式。注意實際使用時要加上「<code>re:</code>」，且通常須把整個表示式加上雙引號，因為正規式示式的特殊字元通常在典籍檢索器也有特殊意義，會導致誤判。例如「<code>re: "五(臟|藏)六(腑|府)"</code>」表示「含有『五(臟|藏)六(腑|府)』」，如果寫成「<code>re: 五(臟|藏)六(腑|府)</code>」會被錯誤解讀成「含有『五』且含有『臟|藏』且含有『六』且含有『腑|府』」。</p>

<ul>
<li><code>[被披][髮發][緩缓]形</code>：尋找「被髮緩形」的各種變形：第一字為「被」或「披」，第二字為「髮」或「發」，第三字為「緩」或「缓」，第四字為「形」。不考慮異體字。</li>
<li><code>(披|被)(髮|發)(緩|缓)形</code>：同上，但各字皆考慮異體字。</li>
<li><code>(?:披|被)(?:髮|發)(?:緩|缓)形</code>：同上，不引用子群組時，這種寫法不記錄子群組的值，可稍微提高執行速度，但寫起來稍麻煩些。</li>
<li><code>(桂枝|麻黃)湯</code>：尋找「桂枝湯」或「麻黃湯」。（分支選擇的範圍越小，執行越快，此寫法比「<code>(桂枝湯|麻黃湯)</code>」快，二者又比非正規表示式的「<code>桂枝湯 OR 麻黃湯</code>」快。）</li>
<li><code>麻黃(?!根)</code>：尋找「麻黃」，但排除「麻黃根」中的「麻黃」。（檢索「<code>re: "麻黃(?!根)"</code>」與「<code>麻黃 -麻黃根</code>」邏輯不同：諸如「麻黃發汗，麻黃根止汗」的文本在檢索前者時可找到，而在檢索後者時會被排除。）</li>
<li><code>(?&lt;!白)附子</code>：尋找「附子」，但排除「白附子」中的「附子」。</li>
<li><code>桂枝(?:加.{0,8}?)?湯</code>：「桂枝湯」或「桂枝加…湯」（「…」長度8字以內）。此式可找到如「桂枝加桂湯」、「桂枝加龍骨牡蠣湯」等等。</li>
<li><code>桂枝.{0,10}?麻黃.{0,10}?附子</code>：「桂枝」後10字內有「麻黃」，再後10字內有「附子」。此式可找到如「桂枝去芍藥加麻黃細辛附子湯方」。</li>
<li><code>(?=桂枝|芍藥|生薑|甘草)(?=.{0,60}?桂枝)(?=.{0,60}?芍藥)(?=.{0,60}?生薑)(?=.{0,60}?甘草).{0,60}(?:桂枝|芍藥|生薑|甘草)</code>：60字範圍內含有「桂枝」、「芍藥」、「生薑」、「甘草」四個關鍵詞，順序不拘。此式可找到一些含有此四種中藥的方子。</li>
<li><code>(?=.{0,15}?朴)(?=.{0,15}?參)(?=.{0,15}?薑)(?=.{0,15}?甘)(?=.{0,15}?半).{0,15}湯</code>：尋找某個由「朴」、「參」、「薑」、「甘」、「半」組成的湯方，各字順序不確定，長度15字以內。</li>
<li><code>「[^」\n]*古方[^」\n]*」</code>：找含有「…古方…」的引言片段。</li>
</ul>

<h2>參考資源</h2>
<p>正規表示式的參考資源極多，可自行尋找適當的學習資源。但要注意不同平台上的正規表示式語法稍有差異（一般稱作不同風味（<b>flavor</b>）），把一個平台的正規表示式搬到另一平台使用，往往須稍做調整。</p>

<p>例如JavaScript的正規表示式常寫成形如「<code>/&lt;div&gt;.*?&lt;\/div&gt;/i</code>」，其中頭尾的「<code>/</code>」是定界字元（<b>delimiter</b>），定界字元不是正規表示式的一部分，只是為了在程式語言中標示正規表示式的開始和結束，若正規表示式也會用到定界字元，則也必須做脫義處理，以免被誤判為結尾；結尾後的一或多個字母是調整正規表示式行為的標幟（<b>flag</b>），或稱修飾符號（<b>modifier</b>）。常見標幟如下：</p>
<ul>
  <li>「<code>i</code>」：無視大小寫。有此標幟時「<code>a</code>」可以匹配「A」；否則不行。</li>
  <li>「<code>m</code>」：多行模式。有此標幟時「<code>^</code>」和「<code>$</code>」表示行首和行尾；否則只表示字串開頭和結尾。</li>
  <li>「<code>s</code>」：有此標幟時「<code>.</code>」可匹配換行字元；否則不匹配。</li>
</ul>

<p>如前所述，支援正規表示式的軟體工具很多，即使不寫程式，正規表示式也可用於文字資料的搜尋及取代，以簡化許多繁瑣的操作。以下是一些不必寫程式也能使用正規表示式的工具：</p>
<ul>
<li>本站的<a href="../book/search.html">典籍檢索器</a>：支援用正規表示式搜尋文本，用的是瀏覽器腳本使用的JavaScript語言。</li>
<li><a href="https://notepad-plus-plus.org/">Notepad++</a>：國人製作的多功能純文字編輯軟體，<a href="https://npp-user-manual.org/docs/searching/#regular-expressions">可使用正規表示式</a>尋找及取代文字，且支援多檔案操作。Notepad++使用的正規表示式是C++的Boost Regex Library。</li>
<li><a href="https://zh-tw.libreoffice.org/">LibreOffice</a>：自由且跨平台的辦公室軟體，可使用正規表示式尋找及取代文字。</li>
<li><a href="https://docs.google.com">Google文件</a>：Google文件和試算表都可以使用正規表示式尋找及取代文字（電腦版）。</li>
<li><a href="https://regex101.com/">Regex101</a>：此網站可輸入正規表示式及文字做尋找及取代，且會分析解釋輸入的正規表示式，並計算執行時間，既可應用亦可學習。可點擊正規表示式輸入區前後的「/」修改定界字元和標幟，點擊左上方的「☰」設定正規表示式的風味。</li>
</ul>

<p>一些可參考的資料如下：</p>
<ol>
<li><a href="https://www.slideshare.net/ccckmit/ss-77167684">用十分鐘學會字串處理的那些事兒</a>：陳鍾誠教授寫的入門教學。</li>
<li><a href="https://www.regular-expressions.info/">Regular-Expressions.info</a>：介紹各種風味的正規表示式，及一些中階技巧。</li>
<li><a href="https://www.rexegg.com/">RexEgg</a>：提供一些進階技巧，一般使用未必需要，但要寫出最佳化、高效能的正規表示式可以參考。</a>
<li><cite lang="en">Mastering Regular Expressions</cite>：聖經級的正規表示式教科書，從基本知識到高深技巧都有。網路上有免費版可供閱讀。</li>
</ol>
